package me.spring.cloud.common.components.ips.search;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;
import lombok.extern.slf4j.Slf4j;
import me.spring.cloud.common.components.ips.common.DataBlock;
import me.spring.cloud.common.components.ips.common.DbConfig;
import me.spring.cloud.common.components.ips.common.IndexBlock;
import me.spring.cloud.common.components.ips.common.IpUtil;

/**
 * ip db searcher class (Not thread safe)
 */
@Slf4j
public class DbSearcher {

  public static final int BTREE_ALGORITHM = 1;
  public static final int BINARY_ALGORITHM = 2;
  public static final int MEMORY_ALGORITYM = 3;

  /**
   * db config
   */
  private DbConfig dbConfig = null;

  /**
   * db file access handler
   */
  private RandomAccessFile raf = null;

  /**
   * header blocks buffer
   */
  private long[] HeaderSip = null;
  private int[] HeaderPtr = null;
  private int headerLength;

  /**
   * super blocks info
   */
  private long firstIndexPtr = 0;
  private long lastIndexPtr = 0;
  private int totalIndexBlocks = 0;

  /**
   * for memory mode the original db binary string
   */
  private byte[] dbBinStr = null;

  /**
   * construct class
   *
   * @param dbConfig
   * @param dbFile
   * @throws FileNotFoundException
   */
  public DbSearcher(DbConfig dbConfig, String dbFile) throws FileNotFoundException {
    this.dbConfig = dbConfig;
    raf = new RandomAccessFile(dbFile, "r");
  }

  /**
   * construct method with self-define std ip2region binary string support Thanks to the issue from
   * Wendal at https://gitee.com/lionsoul/ip2region/issues/IILFL.
   * <p>
   * Note: This construtor should be used for memory search ONLY !!!
   *
   * @param dbConfig
   * @param dbBinStr
   */
  public DbSearcher(DbConfig dbConfig, byte[] dbBinStr) {
    this.dbConfig = dbConfig;
    this.dbBinStr = dbBinStr;

    firstIndexPtr = IpUtil.getIntLong(dbBinStr, 0);
    lastIndexPtr = IpUtil.getIntLong(dbBinStr, 4);
    totalIndexBlocks =
        (int) ((lastIndexPtr - firstIndexPtr) / IndexBlock.getIndexBlockLength()) + 1;
  }

  /**
   * get the region with a int ip address with memory binary search algorithm
   *
   * @param ip
   * @throws IOException
   */
  public DataBlock memorySearch(long ip) throws IOException {
    int blen = IndexBlock.getIndexBlockLength();
    if (dbBinStr == null) {
      dbBinStr = new byte[(int) raf.length()];
      raf.seek(0L);
      raf.readFully(dbBinStr, 0, dbBinStr.length);

      //initialize the global vars
      firstIndexPtr = IpUtil.getIntLong(dbBinStr, 0);
      lastIndexPtr = IpUtil.getIntLong(dbBinStr, 4);
      totalIndexBlocks = (int) ((lastIndexPtr - firstIndexPtr) / blen) + 1;
    }

    //search the index blocks to define the data
    int l = 0, h = totalIndexBlocks;
    long sip, eip, dataptr = 0;
    while (l <= h) {
      int m = (l + h) >> 1;
      int p = (int) (firstIndexPtr + m * blen);

      sip = IpUtil.getIntLong(dbBinStr, p);
      if (ip < sip) {
        h = m - 1;
      } else {
        eip = IpUtil.getIntLong(dbBinStr, p + 4);
        if (ip > eip) {
          l = m + 1;
        } else {
          dataptr = IpUtil.getIntLong(dbBinStr, p + 8);
          break;
        }
      }
    }

    //not matched
    if (dataptr == 0) {
      log.warn("find ip location failed,dataptr not matched. ip:{}",IpUtil.long2ip(ip));
      return null;
    }

    //get the data
    int dataLen = (int) ((dataptr >> 24) & 0xFF);
    int dataPtr = (int) ((dataptr & 0x00FFFFFF));
    int city_id = (int) IpUtil.getIntLong(dbBinStr, dataPtr);
    String region = new String(dbBinStr, dataPtr + 4, dataLen - 4, "UTF-8");

    return new DataBlock(city_id, region, dataPtr);
  }

  /**
   * get the region throught the ip address with memory binary search algorithm
   *
   * @param ip
   * @return DataBlock
   * @throws IOException
   */
  public DataBlock memorySearch(String ip) throws IOException {
    return memorySearch(IpUtil.ip2long(ip));
  }


  /**
   * get by index ptr
   *
   * @param ptr
   * @throws IOException
   */
  public DataBlock getByIndexPtr(long ptr) throws IOException {
    raf.seek(ptr);
    byte[] buffer = new byte[12];
    raf.readFully(buffer, 0, buffer.length);
    long extra = IpUtil.getIntLong(buffer, 8);

    int dataLen = (int) ((extra >> 24) & 0xFF);
    int dataPtr = (int) ((extra & 0x00FFFFFF));

    raf.seek(dataPtr);
    byte[] data = new byte[dataLen];
    raf.readFully(data, 0, data.length);

    int city_id = (int) IpUtil.getIntLong(data, 0);
    String region = new String(data, 4, data.length - 4, "UTF-8");

    return new DataBlock(city_id, region, dataPtr);
  }

  /**
   * get the region with a int ip address with b-tree algorithm
   *
   * @param ip
   * @throws IOException
   */
  public DataBlock btreeSearch(long ip) throws IOException {
    //check and load the header
    if (HeaderSip == null) {
      //pass the super block
      raf.seek(8L);
      byte[] b = new byte[dbConfig.getTotalHeaderSize()];
      // byte[] b = new byte[4096];
      raf.readFully(b, 0, b.length);

      //fill the header
      //b.lenght / 8
      int len = b.length >> 3, idx = 0;
      HeaderSip = new long[len];
      HeaderPtr = new int[len];
      long startIp, dataPtr;
      for (int i = 0; i < b.length; i += 8) {
        startIp = IpUtil.getIntLong(b, i);
        dataPtr = IpUtil.getIntLong(b, i + 4);
        if (dataPtr == 0) {
          break;
        }

        HeaderSip[idx] = startIp;
        HeaderPtr[idx] = (int) dataPtr;
        idx++;
      }

      headerLength = idx;
    }

    //1. define the index block with the binary search
    if (ip == HeaderSip[0]) {
      return getByIndexPtr(HeaderPtr[0]);
    } else if (ip == HeaderSip[headerLength - 1]) {
      return getByIndexPtr(HeaderPtr[headerLength - 1]);
    }

    int l = 0, h = headerLength, sptr = 0, eptr = 0;
    while (l <= h) {
      int m = (l + h) >> 1;

      //perfetc matched, just return it
      if (ip == HeaderSip[m]) {
        if (m > 0) {
          sptr = HeaderPtr[m - 1];
          eptr = HeaderPtr[m];
        } else {
          sptr = HeaderPtr[m];
          eptr = HeaderPtr[m + 1];
        }

        break;
      }

      //less then the middle value
      if (ip < HeaderSip[m]) {
        if (m == 0) {
          sptr = HeaderPtr[m];
          eptr = HeaderPtr[m + 1];
          break;
        } else if (ip > HeaderSip[m - 1]) {
          sptr = HeaderPtr[m - 1];
          eptr = HeaderPtr[m];
          break;
        }
        h = m - 1;
      } else {
        if (m == headerLength - 1) {
          sptr = HeaderPtr[m - 1];
          eptr = HeaderPtr[m];
          break;
        } else if (ip <= HeaderSip[m + 1]) {
          sptr = HeaderPtr[m];
          eptr = HeaderPtr[m + 1];
          break;
        }
        l = m + 1;
      }
    }

    //match nothing just stop it
    if (sptr == 0) {
      return null;
    }

    //2. search the index blocks to define the data
    int blockLen = eptr - sptr, blen = IndexBlock.getIndexBlockLength();
    byte[] iBuffer = new byte[blockLen + blen];    //include the right border block
    raf.seek(sptr);
    raf.readFully(iBuffer, 0, iBuffer.length);

    l = 0;
    h = blockLen / blen;
    long sip, eip, dataptr = 0;
    while (l <= h) {
      int m = (l + h) >> 1;
      int p = m * blen;
      sip = IpUtil.getIntLong(iBuffer, p);
      if (ip < sip) {
        h = m - 1;
      } else {
        eip = IpUtil.getIntLong(iBuffer, p + 4);
        if (ip > eip) {
          l = m + 1;
        } else {
          dataptr = IpUtil.getIntLong(iBuffer, p + 8);
          break;
        }
      }
    }

    //not matched
    if (dataptr == 0) {
      return null;
    }

    //3. get the data
    int dataLen = (int) ((dataptr >> 24) & 0xFF);
    int dataPtr = (int) ((dataptr & 0x00FFFFFF));

    raf.seek(dataPtr);
    byte[] data = new byte[dataLen];
    raf.readFully(data, 0, data.length);

    int city_id = (int) IpUtil.getIntLong(data, 0);
    String region = new String(data, 4, data.length - 4, "UTF-8");

    return new DataBlock(city_id, region, dataPtr);
  }

  /**
   * get the region throught the ip address with b-tree search algorithm
   *
   * @param ip
   * @return DataBlock
   * @throws IOException
   */
  public DataBlock btreeSearch(String ip) throws IOException {
    return btreeSearch(IpUtil.ip2long(ip));
  }

  /**
   * get the region with a int ip address with binary search algorithm
   *
   * @param ip
   * @throws IOException
   */
  public DataBlock binarySearch(long ip) throws IOException {
    int blen = IndexBlock.getIndexBlockLength();
    if (totalIndexBlocks == 0) {
      raf.seek(0L);
      byte[] superBytes = new byte[8];
      raf.readFully(superBytes, 0, superBytes.length);
      //initialize the global vars
      firstIndexPtr = IpUtil.getIntLong(superBytes, 0);
      lastIndexPtr = IpUtil.getIntLong(superBytes, 4);
      totalIndexBlocks = (int) ((lastIndexPtr - firstIndexPtr) / blen) + 1;
    }

    //search the index blocks to define the data
    int l = 0, h = totalIndexBlocks;
    byte[] buffer = new byte[blen];
    long sip, eip, dataptr = 0;
    while (l <= h) {
      int m = (l + h) >> 1;
      raf.seek(firstIndexPtr + m * blen);    //set the file pointer
      raf.readFully(buffer, 0, buffer.length);
      sip = IpUtil.getIntLong(buffer, 0);
      if (ip < sip) {
        h = m - 1;
      } else {
        eip = IpUtil.getIntLong(buffer, 4);
        if (ip > eip) {
          l = m + 1;
        } else {
          dataptr = IpUtil.getIntLong(buffer, 8);
          break;
        }
      }
    }

    //not matched
    if (dataptr == 0) {
      return null;
    }

    //get the data
    int dataLen = (int) ((dataptr >> 24) & 0xFF);
    int dataPtr = (int) ((dataptr & 0x00FFFFFF));

    raf.seek(dataPtr);
    byte[] data = new byte[dataLen];
    raf.readFully(data, 0, data.length);

    int city_id = (int) IpUtil.getIntLong(data, 0);
    String region = new String(data, 4, data.length - 4, "UTF-8");

    return new DataBlock(city_id, region, dataPtr);
  }

  /**
   * get the region throught the ip address with binary search algorithm
   *
   * @param ip
   * @return DataBlock
   * @throws IOException
   */
  public DataBlock binarySearch(String ip) throws IOException {
    return binarySearch(IpUtil.ip2long(ip));
  }

  /**
   * get the db config
   *
   * @return DbConfig
   */
  public DbConfig getDbConfig() {
    return dbConfig;
  }

  /**
   * close the db
   *
   * @throws IOException
   */
  public void close() throws IOException {
    HeaderSip = null;    //let gc do its work
    HeaderPtr = null;
    dbBinStr = null;
    if (raf != null) {
      raf.close();
    }
  }

}
